[Previous] [Next]

The UserControl Object

The UserControl object is the container in which constituent controls are placed. In this sense, it's akin to the Form object, and in fact it shares many properties, methods, and events with the Form object. For example, you can learn its internal dimension using the ScaleWidth and ScaleHeight properties, use the AutoRedraw property to create persistent graphics on the UserControl's surface, and add a border using the BorderStyle property. UserControl objects also support all the graphic properties and methods that forms do, including Cls, Line, Circle, DrawStyle, DrawWidth, ScaleX, and ScaleY.

UserControls support most of the Form object's events, too. For example, Click, DblClick, MouseDown, MouseMove, and MouseUp events fire when the user activates the mouse over the portions of UserControl's surface that aren't covered by constituent controls. UserControl objects also support KeyDown, KeyUp, and KeyPress events, but they fire only when no constituent control can get the focus or when you set the UserControl's KeyPreview property to True.

The Life Cycle of a UserControl Object

UserControl are objects, and as such they receive several events during their lifetime. ActiveX controls actually have a double life because they're also alive when the environment is in design mode.

Creation

Initialize is the first event that a UserControl receives. In this event, no Windows resources have been allocated yet so you shouldn't refer to constituent controls, exactly as you avoid references to controls on a form in the form's Initialize event. For the same reason, the Extender and AmbientProperties objects aren't available in this event. (These objects are described in the following sections.)

After the Initialize event, the UserControl creates all its constituent controls and is ready to be sited on the client form's surface. When the siting completes, Visual Basic fires an InitProperties or ReadProperties event, depending on whether the control has been just dragged on the form from the Toolbox or the form is being reopened from a previous session. During these events, the Extender and the Ambient objects are finally available.

Just before becoming visible, the UserControl module receives the Resize event, and then the Show event. This event is more or less equivalent to the Activate event, which isn't exposed by UserControl modules. Finally the UserControl module receives a Paint event (unless its AutoRedraw property is True).

When a control is re-created at design time because its parent form is closed and then reopened, the complete sequence is repeated with the only differences being that the InitProperties event never fires and the ReadProperties event fires instead, immediately after the Resize event.

Termination

When the developer closes the parent form at design time, or when the program switches to run-time mode, Visual Basic destroys the design-time instance of the ActiveX control. If the developer modified one or more properties in the control, the UserControl module receives a WriteProperties event. During this event, Visual Basic doesn't write anything to the FRM file and simply stores values in the PropertyBag object kept in memory. This event fires only if the programmer modified the attributes of any control on the form (or of the form itself), but not necessarily the UserControl you're working with. A control informs you that one of its properties has changed and that the FRM file needs to be updated by calling the PropertyChanged method. When the control is removed from its container, a Hide event occurs. (ActiveX controls in HTML pages receive this event when the user navigates to another page.) This event broadly corresponds to a form's Deactivate event: The ActiveX control is still in memory, but it isn't visible any longer.

The last event in the life of an ActiveX control is Terminate; during this event, you usually close any open files and return any system resources that you allocated in the Initialize event procedure. The code in this event can't access the Extender and AmbientProperties objects.

Other event sequences

When the developer runs the program, Visual Basic destroys the design-time instance of the ActiveX control, and creates a run-time instance so that the control can receive all the events described previously. The main difference between design-time and run-time instances is that the latter ones never receive a WriteProperties event.

When you reopen the project, you start another special sequence of events: Now a new instance of the control is created, and it receives all the usual events that fire during creation plus a WriteProperties event that serves to update the PropertyBag object in memory.

Finally, when a form module is compiled, Visual Basic creates a hidden instance of it and then queries the properties of all its ActiveX controls so that the compiled program can use the most recent property values. Each ActiveX control receives the Initialize, Resize, ReadProperties, Show, WriteProperties, Hide, and Terminate events. You don't need to perform any special actions during these events. I mention this information only because if your code contains breakpoints or MsgBox commands, they might interfere with the compilation process.

The Extender Object

When you created a UserControl module and you placed an instance of it on a client form, you might have noticed that the Properties window isn't empty, as shown in Figure 17-2. Where did those properties come from?

It turns out that Visual Basic's forms don't use the ActiveX control directly. Instead, they wrap the control within an intermediate object known as the Extender object. This object exposes to the programmer all the properties defined in the ActiveX control, plus a number of properties that Visual Basic adds for its own purposes. For example, Name, Left, Top, and Visible are Extender properties and so you don't have to implement them in the UserControl module. Other Extender properties are Height, Width, Align, Negotiate, Tag, Parent, Container, ToolTipText, DragIcon, DragMode, CausesValidation, TabIndex, TabStop, HelpContextID, and WhatsThisHelpID.

The Extender object also provides methods and events of its own. For example, the Move, Drag, SetFocus, ShowWhatsThis, and ZOrder methods are provided by the container (and in fact, all of them are related to Extender properties in one way or another), as are the GotFocus, LostFocus, Validate, DragDrop, and DragOver events. The perspective of the programmer who uses the ActiveX control is different from the perspective of the control's author, who sees fewer properties, methods, and events.

Click to view at full size.

Reading Extender properties

At times, however, you need to access Extender properties from within the UserControl module. You can do this by means of the Extender property, which returns an object reference to the same Extender interface that's used by the programmer using the control. A typical example of why this might be necessary is when you want your ActiveX control to display its Name property, as most Visual Basic controls do as soon as they're created. To add this feature to the SuperTextBox ActiveX control, you simply need a statement in the InitProperties event procedure:

Private Sub UserControl_InitProperties()
    On Error Resume Next
    Caption = Extender.Name
End Sub

You might wonder why you need an error handler to protect a simple assignment like the preceding one. The reason is that you can't anticipate the environments in which your ActiveX control will be used, so you have no guarantee that the host environment will support the Name property. If it doesn't, the Extender.Name reference fails, and the error will prevent developers from using your control in those environments. In general, different hosts add different Extender members. Visual Basic is probably the most generous environment in terms of Extender properties.

The Extender object is built at run time by the host environment, so the Extender property is defined to return a generic Object. As a result, all the Extender members such as Name or Tag are referenced through late binding. This circumstance explains why accessing those members tends to slow down the code inside your UserControl module and at the same time makes it less robust. Because you can't be sure about which members the Extender object will expose at run time, you shouldn't let your ActiveX control heavily rely on them, and you should always arrange for your control to degrade gracefully when it runs under environments that don't support the features you need.

Finally, keep in mind that a few Extender properties are created only under certain conditions. For example, the Align and Negotiate properties are exposed only if the UserControl's Alignable property is set to True, and the Default and Cancel properties exist only if the UserControl's DefaultCancel property is True. Likewise, the Visible property is unavailable if the InvisibleAtRuntime property is True.

Setting Extender properties

In general, modifying an Extender property from within the UserControl module is considered bad programming practice. I found that under Visual Basic 6 all the Extender properties can be written to, but this might not be true for other environments or for previous versions of Visual Basic itself. In some cases, setting an Extender property provides added functionality. For example, see how you can implement a method that resizes your ActiveX control to fit its parent form:

Sub ResizeToParent()
    Extender.Move 0, 0, Parent.ScaleWidth, Parent.ScaleHeight
End Sub

This routine is guaranteed to work only under Visual Basic because other environments might not support the Move Extender method, and also because you can't be sure that, if a Parent object actually exists, it also supports the ScaleWidth and ScaleHeight properties. If any of the preceding conditions aren't met, this method raises an error 438, "Object doesn't support this property or method."

From the container's point of view, Extender properties have a higher priority than the UserControl's own properties. For example, if the UserControl module exposes a Name property, the client code—at least the client code written in Visual Basic—will actually refer to the Extender property with the same name. For this reason, you should carefully pick the names of your custom properties and stay clear of those automatically added by the most popular containers, such as Visual Basic and the products in the Microsoft Office suite.

TIP
You might intentionally expose properties that are duplicated in the Extender object so that users of your ActiveX control can find that property regardless of what programming language they're using. For example, you can define a Tag property (of type String or Variant) so that your control provides it even when it runs in an environment other than Visual Basic.

The Object property

This visibility rule raises an interesting question: How can the user of the ActiveX control directly access its interface and bypass the Extender object? This is possible thanks to the Object property, another Extender property that returns a reference to the inner UserControl object. This property is sometimes useful to developers who are using the ActiveX control, as in this code:

' Set the Tag property exposed by the UserControl module.
' Raises an error if such property isn't implemented
SuperTextBox1.Object.Tag = "New Tag"

You never need to use the Extender.Object property from within the UserControl module because it returns the same object reference as the Me keyword.

The AmbientProperties Object

An ActiveX control often needs to gather information about the form on which it has been placed. For example, you might want to adapt your ActiveX control to the locale of the user or to the font that's used by the parent form. In some cases, you can gather this information using the Extender or Parent object (for example, using Parent.Font). But there's a better way.

Conforming to the parent form settings

The UserControl object's Ambient property returns a reference to the AmbientProperties object, which in turn exposes several properties that provide information about the environment in which the ActiveX control runs. For example, you can find out what font is being used by the parent form using the Ambient.Font property, and you can determine which colors have been set for the parent form using the Ambient.ForeColor and Ambient.BackColor properties. This information is especially useful when you create the control and you want to conform to the parent form's current settings. See how you can improve the SuperTextBox control so that it behaves like Visual Basic's own controls:

Private Sub UserControl_InitProperties()
    ' Let the label and the text box match the form's font.
    Set CaptionFont = Ambient.Font
    Set Font = Ambient.Font
    ' Let the label's colors match the form's colors.
    CaptionForeColor = Ambient.ForeColor
    CaptionBackColor = Ambient.BackColor
End Sub

The AmbientProperties object is provided by the Visual Basic runtime, which always accompanies the ActiveX control, rather than by the Extender object, which is provided by the host environment. References to the AmbientProperties object rely on early binding, and the Visual Basic runtime automatically supplies a default value for those properties that aren't available in the environment. This detail has two consequences: Ambient properties are faster than Extender properties, and you don't need an error handler when referring to an Ambient property. For example, the AmbientProperties object exposes a DisplayName property, which returns the name that identifies the control in its host environment and lets you initialize the caption of your control:

Private Sub UserControl_InitProperties()
    Caption = Ambient.DisplayName
End Sub

This code should always be preferred to the method based on the Extender.Name property because it delivers a reasonable result under any environment and doesn't require an On Error statement.

Another ambient property that you might find useful is TextAlign, which indicates the preferred text alignment for the controls on the form. It returns one of the following constants: 0-General, 1-Left, 2-Center, 3-Right, 4-FillJustify. If the host environment doesn't provide any information about this feature, Ambient.TextAlign returns 0-General (text to the left, numbers to the right).

If your control contains a PictureBox control, you should set its Palette property equal to the Ambient.Palette property if possible so that the bitmaps on your control don't look strange when the PictureBox constituent control doesn't have the input focus.

The UserMode property

The UserMode property is probably the most important Ambient property because it lets the author of the ActiveX control know whether the control is being used by the developer (UserMode = False) or the user (UserMode = True). Thanks to this property, you can enable different behaviors at design time and run time. If you find it difficult to remember the meaning of the return value of this property, just recall that the "user" in UserMode is the user. See the "Read-Only Properties" section later in this chapter for an example that shows how this property can be useful.

The AmbientChanged event

You can immediately find out when an ambient property changes by trapping the AmbientChanged event. This event receives a string argument equal to the name of the ambient property being changed. For instance, you can allow the BackColor property of your UserControl to automatically match the background color of the parent form by writing this code:

Private Sub UserControl_AmbientChanged(PropertyName As String)
    If PropertyName = "BackColor" Then BackColor = Ambient.BackColor
End Sub

Here's an exception: If you change the parent form's FontTransparent or Palette properties, the ActiveX controls on the form don't receive any notification. The AmbientChanged event is raised both at design time and at run time, so you might need to use the Ambient.UserMode property to differentiate between the two cases.

The AmbientChanged event is most important within user-drawn controls that expose a Default property. These controls must repaint themselves when the value of this property changes:

Private Sub UserControl_AmbientChanged(PropertyName As String)
    If PropertyName = "DisplayAsDefault" Then Refresh
End Sub 

Localizing ActiveX controls

The Ambient.LocaleID property returns a Long value that corresponds to the locale of the program that's hosting the ActiveX control. This value lets you display localized messages in the language of the user—for example, by loading them from a string table, a resource file, or a satellite DLL. But you must account for some rough edges.

When you compile your application, the Visual Basic locale becomes the default locale for the application. But the application that's hosting the control might automatically adapt itself to the language of the user and change its locale accordingly. Inside the Initialize event procedure of the UserControl, the siting procedure hasn't completed yet, so the value returned by the LocaleID ambient property reflects the default locale of the Visual Basic version that compiled it. For this reason, if you want to use this property to load a table of localized messages, you should follow this schema:

Private Sub UserControl_Initialize()
    ' Load messages in the default (Visual Basic's) locale.
    LoadMessageTable Ambient.LocaleID
End Sub    

Private Sub UserControl_InitProperties()
    ' Load messages in the user's locale.
    LoadMessageTable Ambient.LocaleID
End Sub

Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
    ' Load messages in the user's locale.
    LoadMessageTable Ambient.LocaleID
End Sub

Private Sub UserControl_AmbientChanged(PropertyName As String)
    ' Load messages in the new user's locale.
    If PropertyName = "LocaleID" Then LoadMessageTable Ambient.LocaleID 
End Sub

Private Sub LoadMessageTable(LocaleID As Long)
    ' Here you load localized strings and resources.
End Sub

You need to load the message in both the InitProperties and ReadProperties event procedures because the former is invoked when the control is first placed on the form's surface, whereas the latter is invoked any time the project is reopened or the application is executed.

Other ambient properties

The Ambient.ScaleMode property returns a string corresponding to the unit measure currently used in the container form (for example, twip). This value might be useful within messages to the user or the developer. For a way to easily convert from the form's and UserControl's units, see the section "Converting Scale Units."

The Ambient.DisplayAsDefault property is useful only within user-drawn controls whose DefaultCancel property is True. These controls must display a thicker border when their Default extender property becomes True. You usually trap changes to this property in the AmbientChanged event.

The Ambient.SupportsMnemonics property returns True if the environment supports hot keys, such as those that you indicate in a Caption property using the ampersand character. Most containers support this feature, but you can improve the portability of your control if you test this property in the Show event procedure and filter out ampersand characters in your captions if you find that the environment doesn't support hot keys.

The Ambient.RightToLeft property specifies whether the control should display text from right to left, as it might be necessary under Hebrew or Arabic versions of Windows. All the remaining ambient properties—namely, MessageReflect, ShowGrabHandles, ShowHatching, and UIDead—are of no practical use with controls developed with Visual Basic and can be safely ignored.

Implementing Features

The UserControl object exposes many properties, methods, and events that have no equivalent in form modules. In this section, I describe most of them and briefly hint at items that I examine in depth later in the chapter.

Managing the input focus

Understanding how UserControl objects manage the input focus can be a nontrivial task. Several events are related to input focus:

The simplest way to see what actually happens at run time is to create a trace of all the events as they occur when the user visits the constituent controls by pressing the Tab key. I created a simple UserControl named MyControl1 with two TextBox constituent controls on it—named Text1 and Text2—and then added Debug.Print statements in all the event procedures related to focus management. This is what I found in the Immediate window (with some remarks manually added later):

UserControl_EnterFocus   ' The user has tabbed into the control.
MyControl1_GotFocus
Text1_GotFocus
Text1_Validate           ' The user has pressed the Tab key a second time.
Text1_LostFocus
Text2_GotFocus
MyControl1_Validate      ' The user has pressed the Tab key a third time.
Text2_LostFocus
UserControl_ExitFocus
MyControl1_LostFocus
...                      ' The user has pressed Tab several times
UserControl_EnterFocus   ' until the focus reenters the UserControl
MyControl1_GotFocus      ' and the sequence is repeated.
Text1_GotFocus

As you see, the UserControl object gets an EnterFocus just before the ActiveX control raises a GotFocus event in its parent form. Similarly, the UserControl receives an ExitFocus one instant before the ActiveX control raises a LostFocus in the form.

When the focus shifts from one constituent control to another, the control that loses the focus receives a Validate event, but this doesn't happen when the focus leaves the UserControl module. To force the Validate event of the last control in the UserControl, you must explicitly call the ValidateControls method in the UserControl's ExitFocus, which isn't really intuitive. If the ActiveX control includes several controls, it sometimes doesn't make sense to validate them individually in their Validate events. Moreover, if you use the ValidateControls method, you might incorrectly force the validation of a constituent control when the form is being closed (for example, when the user presses Cancel). For all these reasons, it's much better to validate the contents of a multifield ActiveX control only upon a request from the parent form, or more precisely, in the Validate event that the ActiveX control raises in the parent form. If the control is complex, you might simplify the life of programmers by providing a method that performs the validation, as in the following piece of code:

Private Sub MyControl1_Validate(Cancel As Boolean)
    If MyControl1.CheckSubFields = False Then Cancel = True
End Sub

TIP
The Visual Basic documentation omits an important detail about focus management inside ActiveX controls with multiple constituent controls. If the ActiveX control is the only control on the form that can receive the focus and the user presses the Tab key on the last constituent control, the focus won't automatically shift on the first constituent control as the user would expect. So to have such an ActiveX control behave normally, you should add at least one other control on the form. If you don't want to display another control, you should resort to the following trick: Create a CommandButton (or any other control that can get the focus), move it out of sight using a large negative value for the Left or Top property, and then add these statements in its GotFocus event procedure:

Private Sub Command1_GotFocus()
    MyControl1.SetFocus   ' Manually move the focus
                          ' to the ActiveX control.
End Sub

Invisible controls

The InvisibleAtRuntime property permits you to create controls that are visible only at design time, as are the Timer and CommonDialog controls. When the InvisibleAtRuntime property is True, the Extender object doesn't expose the Visible property. You usually want the controls to have a fixed size at design time, and you ensure this result by using the Size method in the UserControl's Resize event:

Private Sub UserControl_Resize()
    Static Active As Boolean
    If Not Active Then Exit Sub        ' Avoid nested calls.
    Active = True
    Size 400, 400
    Active = False
End Sub

Hot keys

If your ActiveX control includes one or more controls that support the Caption property, you can assign each of them a hot key using the ampersand character, as you would do in a regular Visual Basic form. Such hot keys work as you expect, even if the input focus isn't currently on the ActiveX control. As an aside, keep in mind that it's considered bad programming practice to provide an ActiveX control with fixed captions, both because they can't be localized and because they might conflict with other hot keys defined by other controls on the parent form.

If your ActiveX control doesn't include a constituent control with a Caption property, your control responds to the hot keys assigned to the AccessKeys property. For example, you might have a user-drawn control that exposes a Caption property and you want to activate it if the user types the Alt+char key combination, where char is the first character in the Caption. In this circumstance, you must assign the AccessKeys property in the Property Let procedure as follows:

Property Let Caption(New_Caption As String)
    m_Caption = New_Caption
    PropertyChanged "Caption"
    AccessKeys = Left$(New_Caption, 1)
End Property

When the user presses a hot key, an AccessKeyPressed event fires in the UserControl module. This event receives the code of the hot key, which is necessary because you can associate multiple hot keys with the ActiveX control by assigning a string of two or more characters to the AccessKeys property:

Private Sub UserControl_AccessKeyPress(KeyAscii As Integer)
    ' User pressed the Alt + Chr$(KeyAscii) hot key.
End Sub

You can create ActiveX controls that behave like Label controls by setting the ForwardFocus property to True. When the control gets the input focus, it automatically moves it to the control on the form that comes next in the TabIndex order. If the ForwardFocus property is True, the UserControl module doesn't receive the AccessKeyPress event.

Accessing the parent's controls

An ActiveX control can access other controls on its parent form in two distinct ways. The first approach is based on the Controls collection of the Parent object, as this code example demonstrates:

' Enlarge or shrink all controls on the parent form except this one.
Sub ZoomControls(factor As Single)
    Dim ctrl As Object
    For Each ctrl In Parent.Controls
        If Not (ctrl Is Extender) Then
            ctrl.Width = ctrl.Width * factor
            ctrl.Height = ctrl.Height * factor
        End if
    Next
End Sub

The items in the Parent.Controls collection are all Extender objects, so if you want to sort out the ActiveX control that's running the code you must compare each item with the Extender property, not with the Me keyword. The problem with this approach is that it works only under Visual Basic (more precisely, only under environments for which there is a Parent object that exposes the Controls collection).

The second approach is based on the ParentControls property. Unlike the Parent.Controls collection, this property is guaranteed to work with all containers. The items in the Parent.Controls collection contain the parent form itself, but you can easily filter it out by comparing each reference with the Parent object (if there is one).

Converting scale units

In the interaction with the container application, the code in the ActiveX control often has to convert values from the UserControl's coordinate system to the parent form's system by using the ScaleX and ScaleY methods. This is especially necessary in mouse events, where the container expects that the x and y coordinates of the mouse are measured in its current ScaleMode. While you can use the Parent.ScaleMode property to retrieve a Visual Basic form's ScaleMode, this approach fails if the control is running inside another container—for example, Internet Explorer. Fortunately, the ScaleX and ScaleY methods also support the vbContainerPosition constant:

' Forward the MouseDown event to the container, but convert measure units.
Private Sub UserControl_MouseDown(Button As Integer, Shift As Integer, _
    X As Single, Y As Single)
    RaiseEvent MouseDown(Button, Shift, _
        ScaleX(X, vbTwips, vbContainerPosition), _
        ScaleY(Y, vbTwips, vbContainerPosition))
End Sub

When you're raising mouse events from within a constituent control, things are a bit more complicated because you also need to keep the control's offset from the upper left corner of the UserControl's surface:

Private Sub Private Sub Text1_MouseDown(Button As Integer, _
    Shift As Integer, X As Single, Y As Single)
    RaiseEvent MouseDown(Button, Shift, _
        ScaleX(Text1.Left + X, vbTwips, vbContainerPosition), _
        ScaleY(Text1.Top + Y, vbTwips, vbContainerPosition))
End Sub

The ScaleX and ScaleY methods support an additional enumerated constant, vbContainerSize, that you should use when converting a size value (as opposed to a coordinate value). The vbContainerPosition and vbContainerSize constants deliver different results only when the container uses a custom ScaleMode. The ActiveX Control Interface Wizard doesn't address these subtleties, and you must manually edit the code that it produces.

Other properties

If the Alignable property is True, the ActiveX control—more precisely, its Extender object—exposes the Align property. Similarly, you should set DefaultCancel to True if the control has to expose the Default and Cancel properties. This setting is necessary when the ActiveX control should behave like a standard CommandButton and works only if ForwardFocus is False. If the ActiveX control's Default property is True and the user presses Enter, the click will be received by the constituent control whose Default property is also True. If there aren't any constituent controls that support the Default or Cancel properties, you can trap the Enter or Escape key in the AccessKeyPress event.

If the CanGetFocus is False, the UserControl itself can't get the input focus and the ActiveX control won't expose the TabStop property. You can't set this property to False if one or more constituent controls can receive the focus. The opposite is also true: You can't place constituent controls that can receive the focus on a UserControl whose CanGetFocus property is False.

The EventsFrozen property is a run-time property that returns True when the parent form ignores events raised by the UserControl object. This happens, for instance, when the form is in design mode. At run time, you can query this property to find out whether your RaiseEvent commands will be ignored so that you can decide to postpone them. Unfortunately, there's no safe way to find out when the container is again ready to accept events, but you can learn when a paused program has restarted by watching for a change in the UIDead property in the AmbientChanged event.

You can create controls that can be edited at design time by setting the EditAtDesignTime property to True. You can right-click on such controls at design time and select the Edit command to enter edit mode. While the control is in edit mode, it reacts exactly as it does at run time although it doesn't raise events in its container. (The EventsFrozen property returns True.) You exit edit mode when you click anywhere on the form outside the control. In general, writing a control that can be edited at design time isn't a simple task: for example, you must account for all the properties that aren't available at design time and that raise an error if used when Ambient.UserMode returns False.

The ToolboxBitmap property lets you assign the image that will be used in the Toolbox window. You should use 16-by-15-pixel bitmaps, but bitmaps of different size are automatically scaled. You shouldn't use icons because they don't scale well to that dimension. The lower left pixel in the bitmap defines its transparent color.

The ContainerHwnd property is available only through code and returns the Windows handle of the ActiveX control's container. If the control is hosted in a Visual Basic program, this property corresponds to the value returned by the Extender.Container.hWnd property.

The UserControl object exposes a few other properties, which let you create windowless controls, container controls, and transparent controls. I'll cover them later in this chapter.